Skip to content

Visualizer: mode «Baum» (feature tree), and why a call stays a box - #53

Merged
cedricvogel-leo merged 1 commit into
mainfrom
feature/tree-mode
Nov 6, 2026
Merged

cedricvogel-leo merged 1 commit into
mainfrom
feature/tree-mode

Conversation

@cedricvogel-leo

@cedricvogel-leo cedricvogel-leo commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

P34 A: the visualizer's third mode «Baum», plus a note at every call that «Ersetzung» or «Baum» keeps as a box.

1. Mode «Baum» (capability tree, embed features=tree)

Off by default and not part of «everything». Without tree there is no third button, and a stored or preset visualizer=tree shows «Ersetzung».

Steps for an expression e: this applies to an expression statement, the value of an assignment or return, and an if's or while's condition. A bare name or literal gets no tree.

  1. Open: one step replaces e by its tree, in a box at e's place in the line.
  2. Reductions: exactly «Ersetzung»'s steps, each one a subtree becoming its value node. A node without a step of its own (a display, -x) becomes its value together with its last child.
  3. Close: one step from the trivial tree to the value in the line.
Example «Ersetzung» «Baum»
x = 2 + 3 * 4 3 5
x = (2 + 3) * 4 3 5
y = quadrat(2 + 1) 5 7
notenschnitt([4, 5, 4.5]) (local variables) 13 21 (the call's tree plus 3 trees in its box)

How it shows:

  • Calls: a call to a function whose body is return <expression> is replaced by that expression's tree, with the parameters' values filled in. Any other call stays a node, and its box from «Kasten» hangs below the tree; inside the box, each line gets its own tree.
  • Labels: Python's own, e.g. +, quadrat, [ ], .replace, round with a ndigits= child. A name is a leaf.
  • No parentheses: (2 + 3) * 4 and 2 + 3 * 4 differ only in shape.
  • Same as the chain: the tree is built from the syntax tree and the same values «Ersetzung» shows, so the two can't diverge. The check compares the tree's text at every step with the → chain.
  • Stable positions:
    • A tree is laid out with our van der Ploeg port, at the step where it opens or where an inline call is replaced.
    • Every node gets room for the widest text it will show until the tree next grows. That room matters when a name becomes a long value (noten → [4, 5, 4.5]).
    • A reduction therefore moves no node; only those growth steps lay it out again.
    • A tree keeps its height until it closes.
  • Animation: nodes glide, a reduced subtree's nodes move up into it and fade, and new nodes fade in. This follows the «Animationen» option and is off with prefers-reduced-motion.
  • Steady height (Visualizer: one height while stepping (embed) #52): it also holds in «Baum», including the cap and scrolling inside.
  • Screen readers: each tree is an image described by its text at that step, e.g. 2 + 12.
  • Narrow view: a tree is at most as wide as the visualizer shows from where it starts. A wider one scrolls in itself as in B: root in the middle, hints at the edges, arrow keys.

Code:

  • src/treeMode.ts (new): treeModeSteps, buildExpressionTree, episodeTreeLayout.
  • Frame splices the tree into its line the way it splices a box.
  • TreeView can take a precomputed layout and animates changes.
  • layoutTree can reserve room per node.

2. Why a call stays a box («Ersetzung» and «Baum»)

The outermost box now says at its top why the call isn't replaced. The mode button stays pressable. The runtime gives the reason on the call's step (boxReason), and visualizerBoxReason words it in German and English:

Reason German
local variables or a loop notenschnitt hat lokale Variablen – der Aufruf wird als Kasten gezeigt.
an if/match fak wählt mit if, was ausgeführt wird – …
several statements f hat mehrere Anweisungen – …
no return <Ausdruck> f gibt keinen Ausdruck mit return zurück – …
a return expression that can't be written out (e.g. a comprehension) f gibt einen Ausdruck zurück, der sich nicht mit Werten ausschreiben lässt – …

This replaces «Der Kasten zeigt, welches return ausgeführt wird.», which stays as the fallback without a reason.

Checks

  • npm run test:tree (new, in CI): runs on traces of the real runtime under CPython. It checks the four sketch examples, a recursion and [x, -x, 2]:
    • «Baum»'s steps are «Ersetzung»'s, the same objects in the same order, plus one open and one close per tree.
    • the exact step patterns;
    • the labels (+(2, *(3, 4)) against *(+(2, 3), 4));
    • the tree's text step by step equals the → chain;
    • no node moves in a reduction.
  • check-runtime-box-reasons.py (new, in test:runtime): each reason, and none for the short form.
  • test:console, new «Baum» cases:
    • without tree, only «Kasten» and «Ersetzung» show;
    • the four examples' step counts in both modes;
    • the tree's description per step;
    • node centres stay put across reductions in the real DOM;
    • one embed height for every step (windowHeight=900);
    • the hint at the notenschnitt box in «Ersetzung» and in «Baum».
  • Unchanged and passing: lint (the known warning only), test:runtime, test:protocol, test:hints, test:layout, and all of test:console.

Going live

  • Embed: merging ships it, but nothing changes for courses without features=…,tree except the box note in «Ersetzung», which every course sees.
  • Macro: glpython-liascript#33 passes tree on.
  • IDE: it doesn't switch tree on. It gets the box note in «Ersetzung» with the next bump.

Manual tests

Desktop (Chrome, Safari, Firefox), on an embed with features=visualize,tree:

  1. x = 2 + 3 * 4 in «Baum»: open, 3 * 4 → 12, 2 + 12 → 14, close, assign. The +, 2 and 12 nodes don't move.
  2. x = (2 + 3) * 4: same steps, different shape, no parentheses.
  3. quadrat(2 + 1): quadrat → 3, then the call becomes the tree 3 * 3, then 9.
  4. notenschnitt([4, 5, 4.5]) (local variables): the box hangs below the tree with «notenschnitt hat lokale Variablen …», and its lines have their own trees. Switch to «Ersetzung»: same note.
  5. fak(2) (recursion with if): nested trees and boxes; note «wählt mit if …».
  6. Error c = (a + 1) / b with b = 0: the / node is red, with the message and hint below the line.
  7. Options → Animationen off: no movement. On, with the system's «reduce motion»: no movement.
  8. Switch between the three modes mid-way: the visualizer stays at the same point.
  9. Without tree: two buttons, as before.

iPad (landscape, portrait, rotation), with an external keyboard:

  • A wide tree (x = 1 + 2 * 3 - 4 * 5 + 6 * 7 - 8 * 9) at 420 px starts with its root in the middle and shows the hint at the edges.
  • Tab into it, then ←/→ and Home/End scroll it.
  • Stepping with the buttons keeps the embed's height.

Follow-ups (not done)

  • Connections to boxes: a call's box hangs below the tree without a dotted line to its node, and an inline call's body has no outline with the function's name (sketches c and d). Both are possible.
  • Trees where the tree's box sits: it keeps the height of its opening, so a reduced tree leaves empty room below. That's deliberate, for the steady height.
  • Long traces: in a trace of more than 120 steps, the measurement doesn't pick the steps with the tallest trees first. A taller tree can still grow the view once, as a tall step could before.
  • No tree for a for loop's iterable, an assert or a del.
  • Spoken text: it sets parentheses by precedence, so a redundant pair from the source ((2 * 3) + 4) isn't read out. The chain keeps it.
  • Changed for Kasten too: a child whose call spans several lines is now drawn only on its first line, in every mode. Before, a box could be drawn once per line.

🤖 Generated with Claude Code

«Baum» shows an expression as its syntax tree while it is evaluated:
one step opens the tree, then exactly the substitution mode's steps,
each a subtree becoming its value, then one step closes it. Inline
calls are replaced by their return expression's tree, other calls hang
their box below the tree. A tree is laid out where it opens or grows,
with room for every later value, so reductions move nothing; reduced
subtrees fly up and fade (animations on, no reduced motion). Off by
default (capability tree, embed features=tree).

In «Ersetzung» and «Baum», the outermost box now says why the call isn't
replaced (the call-enter step's boxReason).

Checks: test:tree (steps, texts as the chain, stable positions, on
runtime traces), check-runtime-box-reasons.py, and «Baum» cases in
test:console.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cedricvogel-leo
cedricvogel-leo merged commit 104e6c1 into main Nov 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant